package org.fhnw.aigs.server.gameHandling; import org.fhnw.aigs.server.common.ServerConfiguration; import org.fhnw.aigs.commons.GameMode; import java.lang.reflect.*; import java.net.URLClassLoader; import java.util.ArrayList; import org.fhnw.aigs.commons.*; import org.fhnw.aigs.commons.communication.*; import org.fhnw.aigs.server.common.LogRouter; import org.fhnw.aigs.server.common.LoggingLevel; import org.fhnw.aigs.server.gui.ServerGUI; /** * This class manages all games. It creastes new games, lets players join games, * loads Game classes and terminates games. Once a game has been initialized, * the GameManager does not do much for the game, expect terminating it.<br> * v1.0 Initial release<br> * v1.1 Features added<br> * v1.2 Major changes in handlung and additional features<br> * v1.3 Changing of logging * * @author Matthias Stöckli * @version v1.3 (Raphael Stoeckli, 24.02.2015) */ public class GameManager { /** * This list contains all running games. */ public static volatile ArrayList<Game> runningGames = new ArrayList<>(); /** * This list contains all games in which a player still waits for other * players. */ public static volatile ArrayList<Game> waitingGames = new ArrayList<>(); /** * This list contains a reference to all players on the server. */ public static volatile ArrayList<Player> allPlayers = new ArrayList<>(); /** * This method initializes a game. As soon as a client sends a JoinMessage, * it is passed to the game manager. The manager will check whether there * are already games of the same type which could be joined. If that is not * the case, it starts a new party. If the game is a single player game, the * game will also be started immediately. * * @param joinMessage The JoinMessage sent by the player. * @param player A reference to the player who sent the JoinMessage. * @param partyName If desired, the player can join a named party. * @return The newly created game. */ public static synchronized Game joinGame(JoinMessage joinMessage, Player player, String partyName) { Game joinedGame; String gameName = joinMessage.getGameName(); GameMode gameMode = joinMessage.getGameMode(); JoinType joinType = joinMessage.getJoinType(); String message = ""; boolean gameCreated = false; if ((joinType == JoinType.CreateNewGame || joinType == JoinType.CreateNewPrivateGame) && gameMode != GameMode.SinglePlayer) // New (Multiplayer) { Game existing = checkIfPartyAlreadyExists(gameName, partyName); if (existing != null) // Game already exists { //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.WARNING, "Can not create the party '{0}' of the type {1}, because this party already exists.", new Object[]{partyName, gameName}); LogRouter.log(GameManager.class.getName(), LoggingLevel.waring, "Can not create the party '{0}' of the type {1}, because this party already exists.", new Object[]{partyName, gameName}); JoinResponseMessage response = new JoinResponseMessage(joinType, gameMode, false, false, "The party '" + partyName + "' could not be created, because this party name already exists"); response.send(player.getSocket(), player); return null; // Send Message to user: Game with this name can not be creted because it already exists } else { joinedGame = createNewGame(gameName, gameMode, partyName, player); if (joinedGame != null) { if (joinType == JoinType.CreateNewPrivateGame) { joinedGame.setPrivateGame(true); } else { joinedGame.setPrivateGame(false); } joinedGame.addPlayer(player); //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.INFO, "{0} created and joined a new {1} party with the name '{2}'!", new Object[]{player.toString(), gameName, partyName}); LogRouter.log(GameManager.class.getName(), LoggingLevel.game, "{0} created and joined a new {1} party with the name '{2}'!", new Object[]{player.toString(), gameName, partyName}); gameCreated = true; } } } else if (joinType == JoinType.JoinParticularGame && gameMode != GameMode.SinglePlayer) // Existing (Multiplayer) --> Private game { Game existing = checkIfPartyAlreadyExists(gameName, partyName); if (existing == null) // Game does not exist { boolean isRunning = checkIfPartyIsRunning(gameName, partyName); JoinResponseMessage response = null; if (isRunning == true) { //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.WARNING, "Can not join the party '{0}' of the type {1}, because this party has already started.", new Object[]{partyName, gameName}); LogRouter.log(GameManager.class.getName(), LoggingLevel.waring, "Can not join the party '{0}' of the type {1}, because this party has already started.", new Object[]{partyName, gameName}); response = new JoinResponseMessage(joinType, gameMode,false, false, "The party '" + partyName + "' could not be joined, because it has already started"); } else { //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.WARNING, "Can not join the party '{0}' of the type {1}, because this party does not exist.", new Object[]{partyName, gameName}); LogRouter.log(GameManager.class.getName(), LoggingLevel.waring, "Can not join the party '{0}' of the type {1}, because this party does not exist.", new Object[]{partyName, gameName}); response = new JoinResponseMessage(joinType, gameMode,false, false, "The party '" + partyName + "' could not be joined, because this party name does not exist"); } response.send(player.getSocket(), player); return null; // Send Message to user: No game with this name exists } else { joinedGame = joinParty(gameName, gameMode, player, partyName, false); if (joinedGame != null) { //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.INFO, "{0} joined the existing {1} party with the name '{2}'!", new Object[]{player.toString(), gameName, partyName}); LogRouter.log(GameManager.class.getName(), LoggingLevel.game, "{0} joined the existing {1} party with the name '{2}'!", new Object[]{player.toString(), gameName, partyName}); } else { message = "Could not join the party"; } } } else // Join or create (public game) + Singleplayer { if (gameMode == GameMode.SinglePlayer || checkIfPartyIsWaiting(gameName, true) == false) { partyName = getRandomPartyName(gameName, partyName); // Update party name joinedGame = createNewGame(gameName, gameMode, partyName, player); if (joinedGame != null) { joinedGame.addPlayer(player); if (gameMode == GameMode.SinglePlayer) { User aiDummy = new User(gameName + "-AI", ""); // Create a AI dummy for visual purpose aiDummy.setAI(true); User.addUserToUserList(aiDummy); // Add dummy AI user to User list joinedGame.setPrivateGame(true); // Single player is always private } else { joinedGame.setPrivateGame(false); // Create a public game } //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.INFO, "{0} created and joined a new {1} party with the name '{2}'!", new Object[]{player.toString(), gameName, partyName}); LogRouter.log(GameManager.class.getName(), LoggingLevel.game, "{0} created and joined a new {1} party with the name '{2}'!", new Object[]{player.toString(), gameName, partyName}); gameCreated = true; } else { message = "Could not create the party"; gameCreated = false; } } else // Multiplayer or waiting games (public) { joinedGame = joinRandomGame(gameName, gameMode, player); if (joinedGame != null) { //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.INFO, "{0} joined a random party in multiplayer {1}", new Object[]{player.getName(), gameName}); LogRouter.log(GameManager.class.getName(), LoggingLevel.game, "{0} joined a random party in multiplayer {1}", new Object[]{player.getName(), gameName}); } else { message = "Could not join a random party"; } gameCreated = false; } } if (joinedGame == null) { JoinResponseMessage response = new JoinResponseMessage(joinType, gameMode,false, false, message); response.send(player.getSocket(), player); return null; } // Check if the game has enough participants, if so, remove it from the // waiting games list and add it to the running games list. if (joinedGame.hasEnoughParticipants()) { waitingGames.remove(joinedGame); runningGames.add(joinedGame); if(ServerConfiguration.getInstance().getIsConsoleMode() == false){ // Add the game to the GUI ServerGUI.getInstance().addGameToList(joinedGame,false); ServerGUI.getInstance().removeGameFromList(joinedGame, true); } // If there is an exception in the initialization process, report it try { joinedGame.initialize(); } catch (Exception ex) { //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.SEVERE, "Could not initialize game.", ex); LogRouter.log(GameManager.class.getName(), LoggingLevel.severe, "Could not initialize game.", ex); ExceptionMessage exceptionMessage = new ExceptionMessage(ex); exceptionMessage.send(player.getSocket(), player); } //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.INFO, "Initialized {0} (ID {1})", new Object[]{gameName, joinedGame.getId()}); LogRouter.log(GameManager.class.getName(), LoggingLevel.info, "Initialized {0} (ID {1})", new Object[]{gameName, joinedGame.getId()}); } JoinResponseMessage response = new JoinResponseMessage(joinType, true, gameCreated); response.send(player.getSocket(), player); return joinedGame; } /** * Joins the first public game in the list of waiting games with the same gameName. * * @param gameName The game's name. * @param gameMode The game mode. * @param player The player who wants to join the game. * @return The game to be joined. */ private static Game joinRandomGame(String gameName, GameMode gameMode, Player player) { for (int i = 0; i < waitingGames.size(); i++) { Game waitingGame = waitingGames.get(i); if (waitingGame.isPrivateGame() == true) // Skip all private games { continue; } // Check for running games of specified type and join it, if it matches if (waitingGame.getGameName().equals(gameName)) { waitingGame.addPlayer(player); return waitingGame; } } return null; // This should never happen. } /** * Joins a named game, with the defined party name. * The purpose of this method is to allow players to play the game with an * exactly defined set of people. This could easily be used to build a * lobby-like environment. * * @param gameName The game's name. * @param gameMode The game mode. * @param player The player who wants to join the party/game. * @param partyName The name of the party. * @param createNew If true, a new game will be crated if the party with the defined party name was not found. Otherwise null will be returned * @return The game to be joined or null if no game to join (see createNew) was found. */ private static Game joinParty(String gameName, GameMode gameMode, Player player, String partyName, boolean createNew) { Game waitingGame = checkIfPartyAlreadyExists(gameName, partyName); if (waitingGame == null) { if (createNew == true) { return createNewGame(gameName, gameMode, partyName, player); } else { return null; } } else { waitingGame.addPlayer(player); return waitingGame; } } /** * Gets a free random game name * @param gameName Name of the game (type) * @param template Template to build the party name. A number will appended to the template * @return A unique party name */ private static String getRandomPartyName(String gameName, String template) { int counter = 1; String name = template; while(true) { if (checkIfPartyAlreadyExists(gameName, name)!= null) // waiting games { counter++; name = template + "(" + Integer.toString(counter) + ")"; continue; } if (checkIfPartyIsRunning(gameName, name) == true) // running games { counter++; name = template + "(" + Integer.toString(counter) + ")"; continue; } break; } return name; } /** * Method to check whether a party of the defined game type currently is waiting. This method is used to find random waiting games. * @param gameName Name of the game (type) * @param onlyPublicGames If true, only public parties will be considered, otherwise all games will be considered * @return True, if at least one party is waiting, otherwise false */ private static boolean checkIfPartyIsWaiting(String gameName, boolean onlyPublicGames) { if (waitingGames.isEmpty() == true) { return false; } for (int i = 0; i < waitingGames.size(); i++) { if (onlyPublicGames == true && waitingGames.get(i).isPrivateGame() == true) { continue; } if (waitingGames.get(i).getGameName().equals(gameName)) { return true; } } return false; } /** * Method to check whether a party of the defined game type and party name currently is running * @param gameName Name of the game (type) * @param partyName Name of the party to check * @return True, if a party with the defined name and type is running, otherwise false */ private static boolean checkIfPartyIsRunning(String gameName, String partyName) { for (int i = 0; i < runningGames.size(); i++) { if (runningGames.get(i).getGameName().equals(gameName) && runningGames.get(i).getPartyName().equals(partyName) ) { return true; } } return false; } /** * Method to check whether a party of the defined game type and name is waiting * @param gameName Name of the game (type) * @param partyName Name of the party to check * @return The game object of the defined type and party name if existing (waiting), otherwise null */ private static Game checkIfPartyAlreadyExists(String gameName, String partyName) { Game waitingGame; for (int i = 0; i < waitingGames.size(); i++) { waitingGame = waitingGames.get(i); // Check for running games of specified type and party name and // join it, if it matches. if (waitingGame.getGameName().equals(gameName) && waitingGame.getPartyName().equals(partyName)) { return waitingGame; } } return null; } /** * Creates a new game based on the game's name, game mode and a party name * and adds it to the waiting games list. * * @param gameName The game's name. * @param gameMode The game mode. * @param partyname The name of a party (can be null). * @param player The player who creates the game. * @return The newly creasted game. */ private static Game createNewGame(String gameName, GameMode gameMode, String partyname, Player player) { Game newGame = loadGameFromJar(gameName, player); if (newGame == null) { return null; } // <-- For exception handling purposes. newGame.setGameMode(gameMode); newGame.setPartyName(partyname); waitingGames.add(newGame); if(ServerConfiguration.getInstance().getIsConsoleMode() == false){ // Add the game to the GUI ServerGUI.getInstance().addGameToList(newGame,true); } return newGame; } /** * Loads a game from a jar file. This method is being called if a new game * is created. The method will use an URLClassLoader to load the class. * * @param gameName The game's name. * @param player The player who creates the game. * @return The newly created game. */ @SuppressWarnings("unchecked") private static Game loadGameFromJar(String gameName, Player player) { Game loadedGame = null; // Get the class loader of the game (if there is any) and load the class // "GameLogic". If there is no classloader yet, the GameLoader will create // a new one. try { // Load the GameLogic class. URLClassLoader loader = GameLoader.getClassLoaderByName(gameName); Class<Game> gameClazz; gameClazz = (Class<Game>) Class.forName("org.fhnw.aigs." + gameName + ".server.GameLogic", true, loader); // Changed package order v1.1 // Create a new game instance using reflection. // Use an empty, zero-argument constructor for that purpose. Constructor constructor = gameClazz.getConstructor(new Class[]{}); loadedGame = (Game) constructor.newInstance(new Object[]{}); } catch (ClassNotFoundException ex) { //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.SEVERE, "There is no game class with this name."); LogRouter.log(GameManager.class.getName(), LoggingLevel.severe, "There is no game class with this name."); ForceCloseMessage forceCloseMessage = new ForceCloseMessage("There is no GameLogic class for this game."); forceCloseMessage.send(player.getSocket(), player); } catch (InvocationTargetException | IllegalArgumentException | IllegalAccessException | InstantiationException | SecurityException | NoSuchMethodException ex) { //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.SEVERE, "Game could not be instantiated. Check the constructor of your GameLogic class." //LOG// + "Most probably the constructor of the GameLogic class is not parameterless.", ex); LogRouter.log(GameManager.class.getName(), LoggingLevel.severe, "Game could not be instantiated. Check the constructor of your GameLogic class. Most probably the constructor of the GameLogic class is not parameterless.", ex); ExceptionMessage exceptionMessage = new ExceptionMessage((Exception) ex.getCause()); exceptionMessage.send(player.getSocket(), player); } catch (NullPointerException ex) { //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.SEVERE, "There is no GameLogic class for this game:" + gameName); LogRouter.log(GameManager.class.getName(), LoggingLevel.severe, "There is no GameLogic class for this game:" + gameName); ForceCloseMessage forceCloseMessage = new ForceCloseMessage("There is no GameLogic class for this game: " + gameName); forceCloseMessage.send(player.getSocket(), player); } catch (Exception ex) // All other exceptions { //LOG//Logger.getLogger(GameManager.class.getName()).log(Level.SEVERE, "An unknown error occured: " + ex.getMessage()); LogRouter.log(GameManager.class.getName(), LoggingLevel.severe, "An unknown error occured: " + ex.getMessage()); ForceCloseMessage forceCloseMessage = new ForceCloseMessage("An unknown error occured whle loading the game. Please look at the logs"); forceCloseMessage.send(player.getSocket(), player); } return loadedGame; } /** * Terminates a game and removes the terminating player before a message is * sent to all players. This method is used when a single player can be * identified as source. This method sends a {@link ForceCloseMessage} to * all players. * * @param game The game to be terminated. * @param terminatingPlayer The player who closed the game or is otherwise * responsible for the end of a game. * @param reason The reason why the game has to be terminated. */ public static void terminateGame(Game game, Player terminatingPlayer, String reason) { if (game != null) { if (runningGames.contains(game)) { runningGames.remove(game); // Log off all users. for (int i = 0; i < game.getPlayers().size(); i++) { Player player = game.getPlayers().get(i); User.logOffUserByName(player.getName()); } if(ServerConfiguration.getInstance().getIsConsoleMode() == false){ // Refresh GUI ServerGUI.getInstance().removeGameFromList(game, false); //LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.INFO, "Removed the game {0} from the running games list.", game.toString()); LogRouter.log(GameManager.class.getName(), LoggingLevel.game, "Removed the game {0} from the running games list.", game.toString()); } // Remove the player who is the reason on why the game has to be // closed from the game. He or she will then not receive a // ForceCloseMessage. This done because the player could not receive // it anyway and this would just cause other exceptions. game.removePlayer(terminatingPlayer); ForceCloseMessage forceCloseMessage = new ForceCloseMessage(reason); game.sendMessageToAllPlayers(forceCloseMessage); } //Do the same steps if the game is in the list of the waiting games. else if (waitingGames.contains(game)) { waitingGames.remove(game); // Refresh GUI ServerGUI.getInstance().removeGameFromList(game, true); //LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.INFO, "Removed the game {0} from the waiting games list.", game.toString()); LogRouter.log(GameManager.class.getName(), LoggingLevel.game, "Removed the game {0} from the waiting games list.", game.toString()); game.removePlayer(terminatingPlayer); ForceCloseMessage forceCloseMessage = new ForceCloseMessage(reason); game.sendMessageToAllPlayers(forceCloseMessage); } GameManager.cleanUpUsers(); // Clean up user list } } /** * Terminates a game and removes the terminating player before a message is * sent to all players. This method sends a {@link ForceCloseMessage} to all * players. * * @param game The game to be terminated. * @param reason The reason why the game has to be terminated. */ public static void terminateGame(Game game, String reason) { // If the game is null, don't terminate the game. if (game == null) { return; } // Create a ForceCloseMessage and send it to all players. ForceCloseMessage forceCloseMessage = new ForceCloseMessage(reason); game.sendMessageToAllPlayers(forceCloseMessage); // Log off all users. for (int i = 0; i < game.getPlayers().size(); i++) { Player player = game.getPlayers().get(i); User.logOffUserByName(player.getName()); } // End game if it is a running game. if (runningGames.contains(game)) { runningGames.remove(game); if(ServerConfiguration.getInstance().getIsConsoleMode() == false){ ServerGUI.getInstance().removeGameFromList(game, false); } //LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.INFO, "Removed the game {0} from the running games list.", game.toString()); LogRouter.log(GameManager.class.getName(), LoggingLevel.game, "Removed the game {0} from the running games list.", game.toString()); } // End game if it is a waiting game. else if (waitingGames.contains(game)) { waitingGames.remove(game); if(ServerConfiguration.getInstance().getIsConsoleMode() == false){ ServerGUI.getInstance().removeGameFromList(game, true); } //LOG//Logger.getLogger(ServerMessageBroker.class.getName()).log(Level.INFO, "Removed the game {0} from the waiting games list.", game.toString()); LogRouter.log(GameManager.class.getName(), LoggingLevel.game, "Removed the game {0} from the waiting games list.", game.toString()); } } /** * Get a running game by it's ID. * * @param id The game's ID. * @return The game with the specified ID. */ public static Game getRunningGameById(long id) { for (Game game : runningGames) { if (game.getId() == id) { return game; } } return null; } /** * Checks whether a certain user name is already in use. This function is * currently not used. * * @param name The name to be checked. * @return True if the name is already in use, false if not. */ public static boolean checkIfNameAlreadyExists(String name) { // Go through all the running games' players for (Game game : runningGames) { for (Player player : game.getPlayers()) { if (player.getName().equals(name)) { return true; } } } // Go through all the waiting games for (Game game : waitingGames) { for (Player player : game.getPlayers()) { if (player.getName().equals(name)) { return true; } } } // If there is no player with that name, return false return false; } /** * Method to clean up users. All users which are not in a waiting or running * game will be removed from the static user list */ public static void cleanUpUsers() { ArrayList<User> tempUsers = new ArrayList<User>(); boolean found; int len = User.users.size(); // This is more efficient than dynamic peeked in for int lenW = waitingGames.size(); // " int lenR = runningGames.size(); // " Game g; User u; String loginName; int j, k, lenP; for(int i = 0; i < len; i++) { u = User.users.get(i); if (u.isNonPersistentUser() == false) { continue; } // Only remove non persistent users loginName = u.getUserName(); found = false; for(j = 0; j < lenW; j++) // Wating games { g = waitingGames.get(j); lenP = g.getPlayers().size(); for(k = 0; k < lenP; k++) { if (g.getPlayers().get(k).getLoginName().equals(loginName)) { found = true; break; } } if (found == true) // No further check needed -> User still active { break; } } if (found == true) // No further check needed -> User still active { continue; } for(j = 0; j < lenR; j++) // Running games { g = runningGames.get(j); lenP = g.getPlayers().size(); for(k = 0; k < lenP; k++) { if (g.getPlayers().get(k).getLoginName().equals(loginName)) { found = true; break; } } if (found == true) // No further check needed -> User still active { break; } } if (found == false) { tempUsers.add(u); // Mark the user with this index to remove } } len = tempUsers.size(); for(int i = 0; i < len; i++) { u = tempUsers.get(i); User.removeUserFromUserList(u); } } /** * Sends a message to all players on the server. It is not adviced to use * this method. * * @param message The message to be sent. */ public static void sendMessageToAllPlayersOnServer(Message message) { ArrayList<Game> allGames = new ArrayList<>(); allGames.addAll(GameManager.runningGames); allGames.addAll(GameManager.waitingGames); for (Game game : allGames) { game.sendMessageToAllPlayers(message); } } }